Skip to content

Merge : 버전 2.4.0#181

Merged
GulSam00 merged 15 commits into
mainfrom
develop
Apr 6, 2026
Merged

Merge : 버전 2.4.0#181
GulSam00 merged 15 commits into
mainfrom
develop

Conversation

@GulSam00
Copy link
Copy Markdown
Owner

@GulSam00 GulSam00 commented Apr 6, 2026

📌 PR 제목

[Type] : 작업 내용 요약

📌 변경 사항

💬 추가 참고 사항

GulSam00 and others added 15 commits March 27, 2026 23:31
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
[Feat] : 노래별 태그 추출 크롤링 코드 작성 (#173)
[Feat] : 검색 로그 기능 추가 및 UI 개선 (#174)
[Feat] : 검색어 히스토리 컴포넌트 UI 개선 (#175)
[Feat] : 네온 나이트 다크 테마 적용 (#179)
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
singcode Ready Ready Preview, Comment Apr 6, 2026 3:24pm

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Version 2.4.0 - AI Song Tagging, Search Logs, and Neon Night Theme

✨ Enhancement

Grey Divider

Walkthroughs

Description
• AI-powered automatic song tagging using GPT-4o-mini with tag caching
• Search log API and popular search history feature with aggregation
• Neon Night dark theme with vibrant accent colors and glow effects
• Theme toggle in header with expandable button UI pattern
• Terminology change from "좋아요" (like) to "즐겨찾기" (bookmark) throughout
Diagram
flowchart LR
  A["Song Database"] -->|getSongsAllDB| B["Tagging Pipeline"]
  B -->|autoTagSong| C["GPT-4o-mini AI"]
  C -->|tag_ids| D["song_tags Table"]
  E["Search Input"] -->|postSearchLog| F["search_logs Table"]
  F -->|getSearchLog| G["Popular Search History"]
  H["Theme Toggle"] -->|setTheme| I["Neon Night Dark Mode"]
  I -->|CSS Variables| J["Updated UI Colors"]
Loading

Grey Divider

File Changes

1. packages/crawling/src/utils/getSongTag.ts ✨ Enhancement +92/-0

AI-based automatic song tag extraction utility

packages/crawling/src/utils/getSongTag.ts


2. packages/crawling/src/cron/taggingSongs.ts ✨ Enhancement +59/-0

Cron job for batch processing song tagging

packages/crawling/src/cron/taggingSongs.ts


3. apps/web/src/app/api/search/log/route.ts ✨ Enhancement +69/-0

Search log API with GET and POST endpoints

apps/web/src/app/api/search/log/route.ts


View more (44)
4. apps/web/src/queries/searchLogQuery.ts ✨ Enhancement +27/-0

React Query hooks for search log operations

apps/web/src/queries/searchLogQuery.ts


5. apps/web/src/lib/api/searchLog.ts ✨ Enhancement +18/-0

API client functions for search log endpoints

apps/web/src/lib/api/searchLog.ts


6. apps/web/src/app/search/PopularSearchHistory.tsx ✨ Enhancement +44/-0

New component displaying popular search terms

apps/web/src/app/search/PopularSearchHistory.tsx


7. apps/web/src/hooks/useSearchSong.ts ✨ Enhancement +9/-0

Integrated search log mutation into search hook

apps/web/src/hooks/useSearchSong.ts


8. apps/web/src/app/search/HomePage.tsx ✨ Enhancement +27/-14

Added popular search history and improved layout

apps/web/src/app/search/HomePage.tsx


9. apps/web/src/app/search/SearchHistory.tsx ✨ Enhancement +40/-22

Enhanced with hydration state and fixed height

apps/web/src/app/search/SearchHistory.tsx


10. apps/web/src/globals.css ✨ Enhancement +46/-48

Neon Night theme with vibrant accent colors

apps/web/src/globals.css


11. apps/web/src/Header.tsx ✨ Enhancement +94/-17

Theme toggle button and expandable button pattern

apps/web/src/Header.tsx


12. apps/web/src/app/layout.tsx ✨ Enhancement +10/-13

Added ThemeProvider for dark mode support

apps/web/src/app/layout.tsx


13. apps/web/src/app/search/SearchResultCard.tsx ✨ Enhancement +4/-4

Changed heart icon to star for bookmark terminology

apps/web/src/app/search/SearchResultCard.tsx


14. apps/web/src/app/search/MusicCard.tsx ✨ Enhancement +3/-3

Updated hover colors to use accent theme color

apps/web/src/app/search/MusicCard.tsx


15. apps/web/src/queries/likeSongQuery.ts 📝 Documentation +2/-2

Updated comments from like to bookmark terminology

apps/web/src/queries/likeSongQuery.ts


16. apps/web/src/types/song.ts 📝 Documentation +1/-1

Updated comment terminology from like to bookmark

apps/web/src/types/song.ts


17. apps/web/src/app/info/like/page.tsx ✨ Enhancement +1/-1

Changed page title to bookmark management

apps/web/src/app/info/like/page.tsx


18. apps/web/src/app/info/page.tsx ✨ Enhancement +3/-3

Updated menu items and icons for bookmark terminology

apps/web/src/app/info/page.tsx


19. apps/web/src/app/tosing/AddListModal.tsx ✨ Enhancement +1/-1

Updated tab label from like to bookmark

apps/web/src/app/tosing/AddListModal.tsx


20. packages/crawling/src/supabase/getDB.ts ✨ Enhancement +24/-0

Added functions to fetch all songs and tagged song IDs

packages/crawling/src/supabase/getDB.ts


21. packages/crawling/src/supabase/postDB.ts ✨ Enhancement +12/-0

Added function to insert song-tag mappings

packages/crawling/src/supabase/postDB.ts


22. packages/crawling/src/cron/crawlYoutube.ts 🐞 Bug fix +1/-1

Updated import path for validation utility

packages/crawling/src/cron/crawlYoutube.ts


23. packages/crawling/src/cron/crawlYoutubeVerify.ts 🐞 Bug fix +2/-3

Updated import path and increased processing limit

packages/crawling/src/cron/crawlYoutubeVerify.ts


24. packages/crawling/package.json ⚙️ Configuration changes +4/-3

Updated script paths and added tag-songs command

packages/crawling/package.json


25. packages/crawling/CLAUDE.md 📝 Documentation +27/-0

Added documentation for AI tagging pipeline

packages/crawling/CLAUDE.md


26. .github/workflows/tagging_song.yml ✨ Enhancement +43/-0

New workflow for automated song tagging

.github/workflows/tagging_song.yml


27. apps/web/public/changelog.json 📝 Documentation +6/-2

Added version 2.4.0 changelog entry

apps/web/public/changelog.json


28. README.md 📝 Documentation +35/-53

Updated terminology and formatting cleanup

README.md


29. CLAUDE.md 📝 Documentation +1/-1

Updated crawling package description

CLAUDE.md


30. apps/web/src/components/CheckInModal.tsx ✨ Enhancement +2/-2

Updated button styling to use accent color

apps/web/src/components/CheckInModal.tsx


31. apps/web/src/components/MessageDialog.tsx ✨ Enhancement +2/-2

Updated info variant color to use accent

apps/web/src/components/MessageDialog.tsx


32. apps/web/src/components/ThumbUpModal.tsx ✨ Enhancement +1/-1

Updated text color to use muted-foreground

apps/web/src/components/ThumbUpModal.tsx


33. apps/web/src/components/StaticLoading.tsx ✨ Enhancement +1/-1

Updated background to use theme background color

apps/web/src/components/StaticLoading.tsx


34. apps/web/src/Sidebar.tsx ✨ Enhancement +5/-1

Added dark mode hover styling to logout button

apps/web/src/Sidebar.tsx


35. apps/web/src/Footer.tsx ✨ Enhancement +1/-1

Added border-top styling for visual separation

apps/web/src/Footer.tsx


36. apps/web/src/app/search/JpnArtistList.tsx ✨ Enhancement +4/-1

Added dark mode hover styling to button

apps/web/src/app/search/JpnArtistList.tsx


37. apps/web/src/app/info/save/DeleteModal.tsx ✨ Enhancement +1/-1

Updated text color to use muted-foreground

apps/web/src/app/info/save/DeleteModal.tsx


38. apps/web/src/app/api/search/route.ts Formatting +5/-3

Improved type formatting for better readability

apps/web/src/app/api/search/route.ts


39. apps/web/src/app/api/songs/thumb-up/route.ts Formatting +1/-3

Simplified array sorting code formatting

apps/web/src/app/api/songs/thumb-up/route.ts


40. apps/web/public/sitemap-0.xml Miscellaneous +1/-1

Updated lastmod timestamp

apps/web/public/sitemap-0.xml


41. .github/workflows/update_ky_youtube.yml 📝 Documentation +1/-1

Improved step name clarity

.github/workflows/update_ky_youtube.yml


42. .github/workflows/verify_ky_youtube.yml 📝 Documentation +1/-1

Improved step name clarity

.github/workflows/verify_ky_youtube.yml


43. .github/workflows/crawl_recent_tj.yml 📝 Documentation +1/-1

Improved step name clarity

.github/workflows/crawl_recent_tj.yml


44. apps/web/package.json ⚙️ Configuration changes +1/-1

Bumped version to 2.4.0

apps/web/package.json


45. packages/crawling/src/assets/crawlKYValidList.txt Additional files +0/-23527

...

packages/crawling/src/assets/crawlKYValidList.txt


46. packages/crawling/src/assets/crawlKYYoutubeFailedList.txt Additional files +0/-26417

...

packages/crawling/src/assets/crawlKYYoutubeFailedList.txt


47. packages/crawling/src/cron/crawlRecentTJ.ts Additional files +0/-0

...

packages/crawling/src/cron/crawlRecentTJ.ts


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Apr 6, 2026

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX Issues (0)

Grey Divider


Action required

1. Workflow has no triggers 🐞 Bug ☼ Reliability
Description
.github/workflows/tagging_song.yml defines on: but all events are commented out, so the workflow
cannot be triggered (and may be treated as invalid by GitHub Actions). This blocks the tagging
pipeline from running at all.
Code

.github/workflows/tagging_song.yml[R3-7]

+on:
+  # schedule:
+  #   - cron: "0 14 * * *" # 한국 시간 23:00 실행 (UTC+9 → UTC 14:00)
+  # workflow_dispatch:
+
Evidence
The new workflow has an empty trigger section, while existing crawling workflows define active
schedule and workflow_dispatch triggers.

.github/workflows/tagging_song.yml[3-7]
.github/workflows/crawl_recent_tj.yml[3-7]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The new `tagging_song.yml` workflow has no active triggers because `schedule` and `workflow_dispatch` are commented out under `on:`.

### Issue Context
Without at least one trigger, GitHub Actions will never run this workflow (no scheduled runs and no manual dispatch).

### Fix Focus Areas
- .github/workflows/tagging_song.yml[3-7]

### Suggested change
Enable at least `workflow_dispatch` (and optionally `schedule`) under `on:` so the workflow can run.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Search history infinite spinner 🐞 Bug ≡ Correctness
Description
SearchHistory sets isHydrated to true only when searchHistory.length > 0, so first-time users
(or users with cleared history) will see a loading spinner forever. This breaks the empty-state UI
where SearchHistory is rendered when query is empty.
Code

apps/web/src/app/search/SearchHistory.tsx[R10-30]

export default function SearchHistory({ onHistoryClick }: SearchHistoryProps) {
  const { searchHistory, removeFromHistory } = useSearchHistoryStore();
+  const [isHydrated, setIsHydrated] = useState(false);

-  if (searchHistory.length === 0) return null;
+  useEffect(() => {
+    if (searchHistory.length > 0) {
+      setIsHydrated(true);
+    }
+  }, [searchHistory]);

  return (
-    <div className="flex gap-2 overflow-x-auto pb-4">
-      {searchHistory.map((term, index) => (
-        <div
-          key={`${term}-${index}`}
-          className="bg-background flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm"
-        >
-          <span
-            className="hover:text-primary max-w-30 cursor-pointer truncate text-left"
-            onClick={() => onHistoryClick(term)}
-          >
-            {term}
-          </span>
-          <span
-            className="hover:text-destructive cursor-pointer"
-            onClick={() => removeFromHistory(term)}
-            title="검색 기록 삭제"
-          >
-            <X className="h-4 w-4" />
-          </span>
+    <div className="h-30 overflow-hidden">
+      <div className="flex items-center gap-2">
+        <Clock className="h-4 w-4" />
+        <p className="m-2">최근 검색어</p>
+      </div>
+      {!isHydrated ? (
+        <div className="flex items-center justify-center py-4">
+          <Loader2 className="h-5 w-5 animate-spin" />
        </div>
-      ))}
+      ) : (
Evidence
The store’s initial state is an empty array and the component no longer returns early on empty
history; since isHydrated never flips to true when the hydrated value is still empty, the spinner
path stays active indefinitely. The Search page renders SearchHistory in the empty-query state, so
this is user-facing.

apps/web/src/app/search/SearchHistory.tsx[10-53]
apps/web/src/stores/useSearchHistoryStore.ts[14-16]
apps/web/src/app/search/HomePage.tsx[263-268]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`SearchHistory` shows a spinner forever when the persisted history is empty because `isHydrated` is only set when `searchHistory.length > 0`.

### Issue Context
The zustand store initializes `searchHistory` to `[]`. On a first visit (or after clearing history), it remains `[]` after hydration, so the effect never sets `isHydrated` and the UI stays in the loading branch.

### Fix Focus Areas
- apps/web/src/app/search/SearchHistory.tsx[10-53]
- apps/web/src/stores/useSearchHistoryStore.ts[14-16]

### Suggested fix
- Set `isHydrated` to `true` on mount (e.g., `useEffect(() => setIsHydrated(true), [])`) OR use zustand persist hydration helpers (`useSearchHistoryStore.persist.hasHydrated()` / `onRehydrateStorage`) to drive the flag.
- Optionally render an explicit empty state when hydrated but `searchHistory.length === 0`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Search logs full-table scan 🐞 Bug ➹ Performance
Description
GET /api/search/log selects all rows from search_logs, aggregates counts in memory, and returns
the full sorted list; this will degrade/timeout as logs grow. The Search page calls this endpoint in
the empty-query state via PopularSearchHistory, so it impacts real user page loads.
Code

apps/web/src/app/api/search/log/route.ts[R11-28]

+export async function GET(): Promise<NextResponse<ApiResponse<SearchLogCount[]>>> {
+  try {
+    const supabase = await createClient();
+    const { data, error } = await supabase.from('search_logs').select('text');
+
+    if (error) throw error;
+
+    const countMap = new Map<string, number>();
+    for (const row of data) {
+      countMap.set(row.text, (countMap.get(row.text) ?? 0) + 1);
+    }
+
+    const result: SearchLogCount[] = Array.from(countMap, ([text, count]) => ({
+      text,
+      count,
+    })).sort((a, b) => b.count - a.count);
+
+    return NextResponse.json({ success: true, data: result });
Evidence
The API loads the entire search_logs table (select('text')) then loops and sorts in Node.
PopularSearchHistory calls useSearchLogQuery(), and HomePage renders PopularSearchHistory
when query is empty, meaning this expensive endpoint is hit on common page views.

apps/web/src/app/api/search/log/route.ts[11-28]
apps/web/src/app/search/PopularSearchHistory.tsx[11-13]
apps/web/src/app/search/HomePage.tsx[263-268]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`GET /api/search/log` performs an unbounded read and in-memory aggregation over `search_logs`, which will slow down as the table grows.

### Issue Context
This endpoint is called from the Search page empty state (popular searches UI), so its latency directly affects perceived page performance.

### Fix Focus Areas
- apps/web/src/app/api/search/log/route.ts[11-28]
- apps/web/src/app/search/PopularSearchHistory.tsx[11-13]

### Suggested fix
- Move aggregation to the database (e.g., a SQL view/RPC that does `GROUP BY text ORDER BY count DESC LIMIT 10`).
- Enforce server-side limits (top N only) and optionally a time window (e.g., last 7/30 days).
- Consider caching (short TTL) since this is read-heavy UI.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Search log insert poisoning 🐞 Bug ⛨ Security
Description
POST /api/search/log inserts arbitrary text from the request body without type/length validation
or throttling, allowing clients to spam the table and pollute popular-search output. This can cause
DB growth, slower GET aggregation, and user-visible garbage terms.
Code

apps/web/src/app/api/search/log/route.ts[R45-54]

+export async function POST(request: Request): Promise<NextResponse<ApiResponse<void>>> {
+  try {
+    const { text } = await request.json();
+
+    const supabase = await createClient();
+    const { error } = await supabase.from('search_logs').insert({ text });
+
+    if (error) throw error;
+
+    return NextResponse.json({ success: true });
Evidence
The handler trusts request.json() and inserts { text } directly; there are no checks for
empty/oversized/non-string input and no mechanism preventing repeated submissions. The UI posts a
log for every search via useSearchSong, so the table will grow quickly even under normal usage,
and attackers can amplify that.

apps/web/src/app/api/search/log/route.ts[45-52]
apps/web/src/hooks/useSearchSong.ts[83-89]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`POST /api/search/log` allows unvalidated user input to be stored, making the endpoint easy to spam and poison.

### Issue Context
The stored values directly feed the popular-search UI. Also, uncontrolled growth worsens the GET endpoint performance.

### Fix Focus Areas
- apps/web/src/app/api/search/log/route.ts[45-52]
- apps/web/src/hooks/useSearchSong.ts[83-89]

### Suggested fix
- Validate `text`:
 - must be a string
 - trim and reject empty
 - enforce max length (e.g., 50–100 chars)
 - optionally allow only a safe character set
- Add basic abuse protection:
 - rate limit per IP/session (middleware or server-side store)
 - dedupe (e.g., upsert into a counter table instead of inserting a new row per search)
 - optionally require authentication for POST if acceptable for product goals.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Tagging queries truncate data 🐞 Bug ☼ Reliability
Description
The tagging pipeline only loads up to 50,000 songs and 50,000 song_tags rows, so once data exceeds
these limits, older songs can never be tagged and the “already tagged” set becomes incomplete. This
causes missed coverage and potential duplicate insert attempts as the dataset grows.
Code

packages/crawling/src/supabase/getDB.ts[R88-110]

+export async function getSongsAllDB(max: number = 50000) {
+  const supabase = getClient();
+
+  const { data, error } = await supabase
+    .from('songs')
+    .select('id, title, artist')
+    .order('created_at', { ascending: false })
+    .limit(max);
+
+  if (error) throw error;
+
+  return data;
+}
+
+export async function getSongTagSongIdsDB(): Promise<Set<string>> {
+  const supabase = getClient();
+
+  const { data, error } = await supabase.from('song_tags').select('song_id').limit(50000);
+
+  if (error) throw error;
+
+  return new Set(data.map(row => row.song_id));
+}
Evidence
getSongsAllDB() and getSongTagSongIdsDB() both use .limit(50000). taggingSongs.ts relies on
these results to decide what to process/skip, so truncation directly affects correctness when tables
grow beyond the hard cap.

packages/crawling/src/supabase/getDB.ts[88-110]
packages/crawling/src/cron/taggingSongs.ts[11-24]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The tagging cron script depends on truncated queries (`limit(50000)`) for both the song list and the existing-tagged set, which prevents full coverage as data grows.

### Issue Context
Even if the script runs repeatedly, it can only ever see the latest 50k songs (and only the first 50k song_tags rows), so older records can remain unprocessed indefinitely.

### Fix Focus Areas
- packages/crawling/src/supabase/getDB.ts[88-110]
- packages/crawling/src/cron/taggingSongs.ts[11-24]

### Suggested fix
- Implement pagination (Supabase `.range(from, to)` loops) to fetch all songs / all song_tags (or process in stable batches).
- Alternatively, process only untagged songs via a DB query (e.g., left join / `not exists`) so the DB decides what’s missing.
- Add a checkpoint/cursor (e.g., last processed `created_at`/`id`) so recurring runs eventually cover all historical data deterministically.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@GulSam00 GulSam00 merged commit 79af862 into main Apr 6, 2026
4 checks passed
Comment on lines +3 to +7
on:
# schedule:
# - cron: "0 14 * * *" # 한국 시간 23:00 실행 (UTC+9 → UTC 14:00)
# workflow_dispatch:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Workflow has no triggers 🐞 Bug ☼ Reliability

.github/workflows/tagging_song.yml defines on: but all events are commented out, so the workflow
cannot be triggered (and may be treated as invalid by GitHub Actions). This blocks the tagging
pipeline from running at all.
Agent Prompt
### Issue description
The new `tagging_song.yml` workflow has no active triggers because `schedule` and `workflow_dispatch` are commented out under `on:`.

### Issue Context
Without at least one trigger, GitHub Actions will never run this workflow (no scheduled runs and no manual dispatch).

### Fix Focus Areas
- .github/workflows/tagging_song.yml[3-7]

### Suggested change
Enable at least `workflow_dispatch` (and optionally `schedule`) under `on:` so the workflow can run.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 10 to +30
export default function SearchHistory({ onHistoryClick }: SearchHistoryProps) {
const { searchHistory, removeFromHistory } = useSearchHistoryStore();
const [isHydrated, setIsHydrated] = useState(false);

if (searchHistory.length === 0) return null;
useEffect(() => {
if (searchHistory.length > 0) {
setIsHydrated(true);
}
}, [searchHistory]);

return (
<div className="flex gap-2 overflow-x-auto pb-4">
{searchHistory.map((term, index) => (
<div
key={`${term}-${index}`}
className="bg-background flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm"
>
<span
className="hover:text-primary max-w-30 cursor-pointer truncate text-left"
onClick={() => onHistoryClick(term)}
>
{term}
</span>
<span
className="hover:text-destructive cursor-pointer"
onClick={() => removeFromHistory(term)}
title="검색 기록 삭제"
>
<X className="h-4 w-4" />
</span>
<div className="h-30 overflow-hidden">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4" />
<p className="m-2">최근 검색어</p>
</div>
{!isHydrated ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-5 w-5 animate-spin" />
</div>
))}
) : (
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Search history infinite spinner 🐞 Bug ≡ Correctness

SearchHistory sets isHydrated to true only when searchHistory.length > 0, so first-time users
(or users with cleared history) will see a loading spinner forever. This breaks the empty-state UI
where SearchHistory is rendered when query is empty.
Agent Prompt
### Issue description
`SearchHistory` shows a spinner forever when the persisted history is empty because `isHydrated` is only set when `searchHistory.length > 0`.

### Issue Context
The zustand store initializes `searchHistory` to `[]`. On a first visit (or after clearing history), it remains `[]` after hydration, so the effect never sets `isHydrated` and the UI stays in the loading branch.

### Fix Focus Areas
- apps/web/src/app/search/SearchHistory.tsx[10-53]
- apps/web/src/stores/useSearchHistoryStore.ts[14-16]

### Suggested fix
- Set `isHydrated` to `true` on mount (e.g., `useEffect(() => setIsHydrated(true), [])`) OR use zustand persist hydration helpers (`useSearchHistoryStore.persist.hasHydrated()` / `onRehydrateStorage`) to drive the flag.
- Optionally render an explicit empty state when hydrated but `searchHistory.length === 0`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +11 to +28
export async function GET(): Promise<NextResponse<ApiResponse<SearchLogCount[]>>> {
try {
const supabase = await createClient();
const { data, error } = await supabase.from('search_logs').select('text');

if (error) throw error;

const countMap = new Map<string, number>();
for (const row of data) {
countMap.set(row.text, (countMap.get(row.text) ?? 0) + 1);
}

const result: SearchLogCount[] = Array.from(countMap, ([text, count]) => ({
text,
count,
})).sort((a, b) => b.count - a.count);

return NextResponse.json({ success: true, data: result });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Search logs full-table scan 🐞 Bug ➹ Performance

GET /api/search/log selects all rows from search_logs, aggregates counts in memory, and returns
the full sorted list; this will degrade/timeout as logs grow. The Search page calls this endpoint in
the empty-query state via PopularSearchHistory, so it impacts real user page loads.
Agent Prompt
### Issue description
`GET /api/search/log` performs an unbounded read and in-memory aggregation over `search_logs`, which will slow down as the table grows.

### Issue Context
This endpoint is called from the Search page empty state (popular searches UI), so its latency directly affects perceived page performance.

### Fix Focus Areas
- apps/web/src/app/api/search/log/route.ts[11-28]
- apps/web/src/app/search/PopularSearchHistory.tsx[11-13]

### Suggested fix
- Move aggregation to the database (e.g., a SQL view/RPC that does `GROUP BY text ORDER BY count DESC LIMIT 10`).
- Enforce server-side limits (top N only) and optionally a time window (e.g., last 7/30 days).
- Consider caching (short TTL) since this is read-heavy UI.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +45 to +54
export async function POST(request: Request): Promise<NextResponse<ApiResponse<void>>> {
try {
const { text } = await request.json();

const supabase = await createClient();
const { error } = await supabase.from('search_logs').insert({ text });

if (error) throw error;

return NextResponse.json({ success: true });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Search log insert poisoning 🐞 Bug ⛨ Security

POST /api/search/log inserts arbitrary text from the request body without type/length validation
or throttling, allowing clients to spam the table and pollute popular-search output. This can cause
DB growth, slower GET aggregation, and user-visible garbage terms.
Agent Prompt
### Issue description
`POST /api/search/log` allows unvalidated user input to be stored, making the endpoint easy to spam and poison.

### Issue Context
The stored values directly feed the popular-search UI. Also, uncontrolled growth worsens the GET endpoint performance.

### Fix Focus Areas
- apps/web/src/app/api/search/log/route.ts[45-52]
- apps/web/src/hooks/useSearchSong.ts[83-89]

### Suggested fix
- Validate `text`:
  - must be a string
  - trim and reject empty
  - enforce max length (e.g., 50–100 chars)
  - optionally allow only a safe character set
- Add basic abuse protection:
  - rate limit per IP/session (middleware or server-side store)
  - dedupe (e.g., upsert into a counter table instead of inserting a new row per search)
  - optionally require authentication for POST if acceptable for product goals.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant